/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Validate } from '../helper/Validate';
import { StringUtil } from '../internal/StringUtil';
import { Attributes } from './Attributes';
import { NodeUtils } from './NodeUtils';
import List from '@ohos.util.List';
import ArrayList from '@ohos.util.ArrayList';
import { OutputSettings } from './OutputSettings';
// import { Element } from './Element';
import { NodeVisitor } from '../select/NodeVisitor';
import { NodeFilter } from '../select/NodeFilter';
import LinkedList from '@ohos.util.LinkedList';
import { Document } from './Document';
import { FilterResult } from '../select/FilterResult';
import { TextNode } from './TextNode';
import { Range } from './Range';
import hilog from '@ohos.hilog';
import logUtil from '../util/LogUtil';
// import { Elements } from '../select/Elements';

/**
 * The base, abstract Node model. Elements, Documents, Comments etc are all Node instances.
 */
export abstract class Node {
  static readonly EmptyNodes = new List<Node>()
  static readonly EmptyString = ""
  // Nodes don't always have parents
  parentNode: Node
  siblingIndex: number

  /**
   * Default constructor. Doesn't setup base uri, children, or attributes; use with caution.
   */
  protected constructor() {
  }

  /**
   Get the node name of this node. Use for debugging purposes and not logic switching (for that, use instanceof).
   @return node name
   */
  public abstract nodeName(): string;

  /**
   Get the normalized name of this node. For node types other than Element, this is the same as {@link #nodeName()}.
   For an Element, will be the lower-cased tag name.
   @return normalized node name
   @since 1.15.4.
   */
  public normalName():string {
    return this.nodeName()
  }

  /**
   * Check if this Node has an actual Attributes object.
   */
  protected abstract hasAttributes(): boolean;

  /**
   Checks if this node has a parent. Nodes won't have parents if (e.g.) they are newly created and not added as a child
   to an existing node, or if they are a {@link #shallowClone()}. In such cases, {@link #parent()} will return {@code null}.
   @return if this node has a parent.
   */
  public hasParent(): boolean {
    return this.parentNode !== undefined && this.parentNode !== null;
  }

  /**
   * Get an attribute's value by its key. <b>Case insensitive</b>
   * <p>
   * To get an absolute URL from an attribute that may be a relative URL, prefix the key with <code><b>abs</b></code>,
   * which is a shortcut to the {@link #absUrl} method.
   * </p>
   * E.g.:
   * <blockquote><code>String url = a.attr("abs:href");</code></blockquote>
   *
   * @param attributeKey The attribute key.
   * @return The attribute, or empty string if not present (to avoid nulls).
   * @see #attributes()
   * @see #hasAttr(String)
   * @see #absUrl(String)
   */
  public getAttr(attributeKey: string): string {
    Validate.notNull(attributeKey);
    if (!this.hasAttributes()) {
      return Node.EmptyString
    }

    let val = this.attributes().getIgnoreCase(attributeKey);
    if (val.length > 0) {
      return val;
    } else if (attributeKey.startsWith("abs:")) {
      return this.absUrl(attributeKey.substring("abs:".length));
    } else {
      return "";
    }
  }
  /**
   * Get all of the element's attributes.
   * @return attributes (which implements iterable, in same order as presented in original HTML).
   */
  public abstract attributes(): Attributes;

  /**
   Get the number of attributes that this Node has.
   @return the number of attributes
   @since 1.14.2
   */
  public attributesSize() {
    // added so that we can test how many attributes exist without implicitly creating the Attributes object
    return this.hasAttributes() ? this.attributes().size():0
  }
  /**
   * Set an attribute (key=value). If the attribute already exists, it is replaced. The attribute key comparison is
   * <b>case insensitive</b>. The key will be set with case sensitivity as set in the parser settings.
   * @param attributeKey The attribute key.
   * @param attributeValue The attribute value.
   * @return this (for chaining)
   */
  public setAttr(attributeKey: string, attributeValue: string): Node {
    attributeKey = NodeUtils.parser(this).getSettings().normalizeAttribute(attributeKey);
    this.attributes().putIgnoreCase(attributeKey, attributeValue);
    return this;
  }

  /**
   * Test if this element has an attribute. <b>Case insensitive</b>
   * @param attributeKey The attribute key to check.
   * @return true if the attribute exists, false if not.
   */
  public hasAttr(attributeKey: string): boolean {
    Validate.notNull(attributeKey);
    if (!this.hasAttributes()) {
      return false
    }
    if (attributeKey.startsWith("abs:")) {
      let key = attributeKey.substring("abs:".length);
      if (this.attributes().hasKeyIgnoreCase(key) && this.absUrl(key) !== "")
        return true;
    }
    return this.attributes().hasKeyIgnoreCase(attributeKey);
  }

  /**
   * Remove an attribute from this node.
   * @param attributeKey The attribute to remove.
   * @return this (for chaining)
   */
  public removeAttr(attributeKey: string): Node {
    Validate.notNull(attributeKey);
    if (this.hasAttributes()) {
      this.attributes().removeIgnoreCase(attributeKey);
    }
    return this;
  }

  /**
   * Clear (remove) all of the attributes in this node.
   * @return this, for chaining
   */
  public clearAttributes(): Node {
    if (this.hasAttributes()) {
      this.attributes().clearAttributes()
    }
    return this;
  }

  /**
   Get the base URI that applies to this node. Empty string if not defined. Used to make relative links absolute to.
   @return base URI
   */
  public abstract baseUri(): string;

  /**
   * Set the baseUri for just this node (not its descendants), if this Node tracks base URIs.
   * @param baseUri new URI
   */
  public  abstract doSetBaseUri(baseUri: string);

  /**
   Update the base URI of this node and all of its descendants.
   @param baseUri base URI to set
   */
  public setBaseUri(baseUri: string) {
    Validate.notNull(baseUri);
    this.doSetBaseUri(baseUri);
  }

  /**
   * Get an absolute URL from a URL attribute that may be relative (i.e. an <code>&lt;a href&gt;</code> or
   * <code>&lt;img src&gt;</code>).
   * <p>
   * E.g.: <code>String absUrl = linkEl.absUrl("href");</code>
   * </p>
   * <p>
   * If the attribute value is already absolute (i.e. it starts with a protocol, like
   * <code>http://</code> or <code>https://</code> etc), and it successfully parses as a URL, the attribute is
   * returned directly. Otherwise, it is treated as a URL relative to the element's {@link #baseUri}, and made
   * absolute using that.
   * </p>
   * <p>
   * As an alternate, you can use the {@link #attr} method with the <code>abs:</code> prefix, e.g.:
   * <code>String absUrl = linkEl.attr("abs:href");</code>
   * </p>
   *
   * @param attributeKey The attribute key
   * @return An absolute URL if one could be made, or an empty string (not null) if the attribute was missing or
   * could not be made successfully into a URL.
   * @see #attr
   * @see java.net.URL#URL(java.net.URL, String)
   */
  public absUrl(attributeKey: string): string {
    Validate.notEmpty(attributeKey,'Node absUrl attributeKey must not be empty');
    // not using hasAttr, so that we don't recurse down hasAttr->absUrl
    if (!(this.hasAttributes() && this.attributes().hasKeyIgnoreCase(attributeKey))) {
      return ""; // nothing to make absolute with
    } else {
      return StringUtil.resolve(this.baseUri(), this.getAttr(attributeKey));
    }
  }

  protected abstract ensureChildNodes(): List<Node>;

  /**
   Get a child node by its 0-based index.
   @param index index of child node
   @return the child node at this index. Throws a {@code IndexOutOfBoundsException} if the index is out of bounds.
   */
  public childNode(index: number): Node {
    return this.ensureChildNodes().get(index);
  }

  /**
   Get this node's children. Presented as an unmodifiable list: new children can not be added, but the child nodes
   themselves can be manipulated.
   @return list of children. If no children, returns an empty list.
   */
  public childNodes(): List<Node> {
    if (this.childNodeSize() === 0) {
      return Node.EmptyNodes
    }
    return this.ensureChildNodes();
  }

  /**
   * Returns a deep copy of this node's children. Changes made to these nodes will not be reflected in the original
   * nodes
   * @return a deep copy of this node's children
   */
  public childNodesCopy(): List<Node> {
    let nodes = this.ensureChildNodes();
    let children = new List<Node>();
    for (let index = 0; index < nodes.length; index++) {
      children.add(nodes[index])
    }
    return children;
  }

  /**
   * Get the number of child nodes that this node holds.
   * @return the number of child nodes that this node holds.
   */
  public abstract childNodeSize(): number;

  protected childNodesAsArray(): Node[] {
    let cs = this.ensureChildNodes()
    let result:Node[] = []
    for (let index = 0; index < cs.length; index++) {
      result[index] = cs[index]
    }
    return result;
  }

  /**
   * Delete all this node's children.
   * @return this node, for chaining
   */
  public abstract empty(): Node;

  /**
   Gets this node's parent node.
   @return parent node; or null if no parent.
   */
  public parent(): Node {
    return this.parentNode;
  }

  /**
   Gets this node's parent node. Not overridable by extending classes, so useful if you really just need the Node type.
   @return parent node; or null if no parent.
   */
  public getParentNode(): Node {
    return this.parentNode;
  }

  /**
   * Get this node's root node; that is, its topmost ancestor. If this node is the top ancestor, returns {@code this}.
   * @return topmost ancestor.
   */
  public root(): Node {
    let node:Node = null
    let parentNode = this.parentNode
    while (parentNode !== undefined && parentNode !== null) {
      node = parentNode
      parentNode = node.parentNode
    }
    return node === null ? this : node;
  }

  /**
   * Gets the Document associated with this Node.
   * @return the Document associated with this Node, or null if there is no such Document.
   */
  public ownerDocument(): Document | null{
    return  null;
  }

  /**
   * Remove (delete) this node from the DOM tree. If this node has children, they are also removed.
   */
  public remove() {
    if (this.parentNode !== undefined && this.parentNode !== null) {
      this.parentNode.removeChild(this);
    }
  }

  /**
   * Insert the specified HTML into the DOM before this node (i.e. as a preceding sibling).
   * @param html HTML to add before this node
   * @return this node, for chaining
   * @see #after(String)
   */
  public before(html: string | Node): Node {
    if (html instanceof Node) {
      Validate.notNull(html);
      Validate.notNull(this.parentNode);
      // if the incoming node is a sibling of this, remove it first so siblingIndex is correct on add
      if (html.parentNode === this.parentNode) {
        html.remove()
      }
      this.parentNode.addChildren1(this.siblingIndex, [html]);
    } else {
      this.addSiblingHtml(this.siblingIndex, html);
    }
    return this;
  }

  /**
   * Insert the specified HTML into the DOM after this node (i.e. as a following sibling).
   * @param html HTML to add after this node
   * @return this node, for chaining
   * @see #before(String)
   */
  public after(html: string): Node {
    this.addSiblingHtml(this.siblingIndex + 1, html);
    return this;
  }

  /**
   * Insert the specified node into the DOM after this node (i.e. as a following sibling).
   * @param node to add after this node
   * @return this node, for chaining
   * @see #before(Node)
   */
  public after1(node: Node): Node {
    Validate.notNull(node);
    Validate.notNull(this.parentNode);
    // if the incoming node is a sibling of this, remove it first so siblingIndex is correct on add
    if (node.parentNode === this.parentNode) {
      node.remove()
    }
    this.parentNode.addChildren1(this.siblingIndex + 1, [node]);
    return this;
  }

  protected  addSiblingHtml(index: number, html: string) {
    Validate.notNull(html);
    Validate.notNull(this.parentNode);

    let nodes = NodeUtils.parser(this).parseFragmentInput(html, null, this.baseUri());
    let ans: Node[] = []
    for (let index = 0; index < nodes.length; index++) {
      ans[index] = nodes[index];

    }
    this.parentNode.addChildren1(index, ans);
  }

  /**
   Wrap the supplied HTML around this node.
   @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep.
   @return this node, for chaining.
   */
  public wrap(html: string): Node {
    Validate.notEmpty(html,'Node wrap html must not be empty');
    NodeUtils.parser(this).parseFragmentInput(html, null, this.baseUri());
    return this;
  }

  /**
   * Removes this node from the DOM, and moves its children up into the node's parent. This has the effect of dropping
   * the node but keeping its children.
   * <p>
   * For example, with the input html:
   * </p>
   * <p>{@code <div>One <span>Two <b>Three</b></span></div>}</p>
   * Calling {@code element.unwrap()} on the {@code span} element will result in the html:
   * <p>{@code <div>One Two <b>Three</b></div>}</p>
   * and the {@code "Two "} {@link TextNode} being returned.
   *
   * @return the first child of this node, after the node has been unwrapped. Null if the node had no children.
   * @see #remove()
   * @see #wrap(String)
   */
  public unwrap(): Node {
    Validate.notNull(this.parentNode);
    let firstChild = this.firstChild()
    this.parentNode.addChildren1(this.siblingIndex, this.childNodesAsArray());
    this.remove();
    return firstChild;
  }

  nodelistChanged() {
    // Element overrides this to clear its shadow children elements
  }

  /**
   * Replace this node in the DOM with the supplied node.
   * @param in the node that will will replace the existing node.
   */
  public replaceWith(inNode: Node) {
    Validate.notNull(inNode);
    Validate.notNull(this.parentNode);
    this.parentNode.replaceChild(this, inNode);
  }

  setParentNode(parentNode: Node) {
    Validate.notNull(parentNode);
    if (this.parentNode !== undefined && this.parentNode !== null) {
      this.parentNode.removeChild(this);
    }

    this.parentNode = parentNode;
  }

  replaceChild(outNode: Node, inNode: Node) {
    Validate.isTrue(outNode.parentNode == this);
    Validate.notNull(inNode);
    if (outNode === inNode) {
      return
    }
    if (inNode.parentNode !== undefined && inNode.parentNode !== null) {
      inNode.parentNode.removeChild(inNode);
    }

    let index = outNode.siblingIndex;
    this.ensureChildNodes().set(index, inNode);
    inNode.parentNode = this;
    inNode.setSiblingIndex(index);
    outNode.parentNode = null;
  }

  removeChild(outNode: Node) {
    Validate.isTrue(outNode.parentNode === this);
    let index = outNode.siblingIndex;
    this.ensureChildNodes().removeByIndex(index);
    this.reindexChildren(index);
    outNode.parentNode = null;
  }

  addChildren(children: Node[]) {
    //most used. short circuit addChildren(int), which hits reindex children and array copy
    let nodes = this.ensureChildNodes();
    for (let index = 0; index < children.length; index++) {
      const child = children[index];
      this.reparentChild(child);
      nodes.add(child);
      child.setSiblingIndex(nodes.length - 1);
    }
  }

  addChildren1(index: number, children: Node[]) {
    Validate.notNull(children);
    if (children.length == 0) {
      return;
    }
    let nodes = this.ensureChildNodes();

    // fast path - if used as a wrap (index=0, children = child[0].parent.children - do inplace
    let firstParent = children[0].parent();
    if (firstParent !== null && firstParent.childNodeSize() === children.length) {
      let sameList = true;
      let firstParentNodes = firstParent.childNodes();
      // identity check contents to see if same
      let i = children.length;
      while (i-- > 0) {
        if (children[i] !== firstParentNodes.get(i)) {
          sameList = false;
          break;
        }
      }
      firstParent.empty();
      let j = index
      for (let m = 0;m < children.length; m++) {
        nodes.insert(children[m], j++)
      }
      i = children.length;
      while (i-- > 0) {
        children[i].parentNode = this;
      }
      this.reindexChildren(index);
      return;
    }

    Validate.noNullElements(children);
    for (let index = 0; index < children.length; index++) {
      this.reparentChild(children[index])

    }
    let j = index
    for (let m = 0;m < children.length; m++) {
      nodes.insert(children[m], j++)
    }
    this.reindexChildren(index);
  }

  reparentChild(child: Node) {
    child.setParentNode(this);
  }

  private reindexChildren(start: number) {
    let size = this.childNodeSize()
    if (size ===0) {
      return
    }
    let childNodes = this.ensureChildNodes();
    for (let i = start; i < childNodes.length; i++) {
      childNodes.get(i).setSiblingIndex(i);
    }
  }

  /**
   Retrieves this node's sibling nodes. Similar to {@link #childNodes()  node.parent.childNodes()}, but does not
   include this node (a node is not a sibling of itself).
   @return node siblings. If the node has no parent, returns an empty list.
   */
  public siblingNodes(): List<Node> {
    if (this.parentNode === undefined || this.parentNode === null) {
      return new List();
    }
    let nodes = this.parentNode.ensureChildNodes();
    let siblings = new List<Node>();
    for (let i = 0;i < nodes.length; i++) {
      let node = nodes.get(i)
      if (node !== this) {
        siblings.add(node)
      }
    }
    return siblings;
  }

  /**
   Get this node's next sibling.
   @return next sibling, or null if this is the last sibling
   */
  public nextSibling(): Node | null {
    if (this.parentNode === undefined || this.parentNode === null) {
      return null; // root
    }

    let siblings = this.parentNode.ensureChildNodes();
    let index = this.siblingIndex + 1;
    if (siblings.length > index)
      return siblings.get(index);
    else
      return null;
  }

  /**
   Get this node's previous sibling.
   @return the previous sibling, or null if this is the first sibling
   */
  public previousSibling(): Node {
    if (this.parentNode === undefined || this.parentNode === null) {
      return null; // root
    }

    if (this.siblingIndex > 0)
      return this.parentNode.ensureChildNodes().get(this.siblingIndex - 1);
    else
      return null;
  }

  /**
   * Get the list index of this node in its node sibling list. I.e. if this is the first node
   * sibling, returns 0.
   * @return position in node sibling list
   * @see org.jsoup.nodes.Element#elementSiblingIndex()
   */
  public getSiblingIndex(): number {
    return this.siblingIndex;
  }

  setSiblingIndex(siblingIndex: number) {
    this.siblingIndex = siblingIndex;
  }

  /**
   Gets the first child node of this node, or {@code null} if there is none. This could be any Node type, such as an
   Element, TextNode, Comment, etc. Use {@link Element#firstElementChild()} to get the first Element child.
   @return the first child node, or null if there are no children.
   @see Element#firstElementChild()
   @see #lastChild()
   @since 1.15.2
   */
  public firstChild() {
    if (this.childNodeSize() === 0) {
      return null
    }
    return this.ensureChildNodes().get(0)
  }

  /**
   Gets the last child node of this node, or {@code null} if there is none.
   @return the last child node, or null if there are no children.
   @see Element#lastElementChild()
   @see #firstChild()
   @since 1.15.2
   */
  public lastChild():Node {
    let size = this.childNodeSize()
    if (size === 0) {
      return null
    }
    let children = this.ensureChildNodes()
    return children.get(size - 1)
  }


  /**
   Perform the supplied action on this Node and each of its descendants, during a depth-first traversal. Nodes may be
   inspected, changed, added, replaced, or removed.
   @param action the function to perform on the node
   @return this Node, for chaining
   @see Element#forEach(Consumer)
   */
  public forEachNode(action: (node: Node) => void) :Node{
    Validate.notNull(action)
    this.traverse({head(node:Node, depth:number){
      action(node)
    },tail(node:Node, depth:number){}},this)
    return this
  }

  /**
   * Perform a depth-first filtering through this node and its descendants.
   * @param nodeFilter the filter callbacks to perform on each node
   * @return this node, for chaining
   */
  public filter(nodeFilter: NodeFilter): Node {
    Validate.notNull(nodeFilter);
    this.filterNode(nodeFilter, this);
    return this;
  }

  /**
   Get the outer HTML of this node. For example, on a {@code p} element, may return {@code <p>Para</p>}.
   @return outer HTML
   @see Element#html()
   @see Element#text()
   */
  public outerHtml(): string {
    let accum = StringUtil.borrowBuilder();
    this.outerHtml1(accum);
    return StringUtil.releaseBuilder(accum);
  }

  outerHtml1(accum: string[]) {
    this.traverse(new OuterHtmlVisitor(accum, NodeUtils.outputSettings(this)), this);
  }

  /**
   Get the outer HTML of this node.
   @param accum accumulator to place HTML into
   @throws IOException if appending to the given accumulator fails.
   */
  abstract outerHtmlHead(accum: string[], depth: number, out: OutputSettings)

  abstract outerHtmlTail(accum: string[], depth: number, out: OutputSettings);

  /**
   * Write this node and its children to the given {@link Appendable}.
   *
   * @param appendable the {@link Appendable} to write to.
   * @return the supplied {@link Appendable}, for chaining.
   */
  public html(appendable: string[]): string[] {
    this.outerHtml1(appendable);
    return appendable;
  }

  /**
   Get the source range (start and end positions) in the original input source that this node was parsed from. Position
   tracking must be enabled prior to parsing the content. For an Element, this will be the positions of the start tag.
   @return the range for the start of the node.
   @see org.jsoup.parser.Parser#setTrackPosition(boolean)
   @see Element#endSourceRange()
   @since 1.15.2
   */
  public sourceRange() :Range {
    return Range.of(this,true)
  }

  /** Test if this node is not null and has the supplied normal name. */
  static isNode(node:Node,normalName:string):boolean {
    return node !== undefined && node !== null && node.normalName() === normalName
  }

  isNode(normalName:string) : boolean{
    return this.normalName() === normalName
  }

  /** Test if this node is the first child, or first following blank text. */
  isEffectivelyFirst():boolean {
    if (this.siblingIndex === 0) {
      return true
    }
    return false
  }


  /**
   * Gets this node's outer HTML.
   * @return outer HTML.
   * @see #outerHtml()
   */
  public toString(): string {
    return this.outerHtml();
  }

  protected indent(accum: string[], depth: number, out: OutputSettings) {
    accum.push('\n')
    accum.push(StringUtil.padding(depth * out.getIndentAmount()))
  }

  /**
   * Check if this node is the same instance of another (object identity test).
   * @param o other object to compare to
   * @return true if the content of this node is the same as the other
   * @see Node#hasSameValue(Object) to compare nodes by their value
   */
  public equals(o: any): boolean {
    // implemented just so that javadoc is clear this is an identity test
    return this === o;
  }

  /**
   * Check if this node is has the same content as another node. A node is considered the same if its name, attributes and content match the
   * other node; particularly its position in the tree does not influence its similarity.
   * @param o other object to compare to
   * @return true if the content of this node is the same as the other
   */
  public hasSameValue(o: any): boolean {
    if (this === o) return true;
    if (o === undefined || o === null) return false;

    return this.outerHtml() === ((o as Node).outerHtml());
  }

  /**
   * Create a stand-alone, deep copy of this node, and all of its children. The cloned node will have no siblings or
   * parent node. As a stand-alone object, any changes made to the clone or any of its children will not impact the
   * original node.
   * <p>
   * The cloned node may be adopted into another Document or node structure using {@link Element#appendChild(Node)}.
   * @return a stand-alone cloned node, including clones of any children
   * @see #shallowClone()
   */
  public clone(): Node {
    let thisClone = this.doClone(null); // splits for orphan

    // Queue up nodes that need their children cloned (BFS).
    let nodesToProcess = new LinkedList<Node>();
    nodesToProcess.add(thisClone);

    while (nodesToProcess.length > 0) {
      let currParent = nodesToProcess.removeFirst();

      let size = currParent.childNodeSize();
      for (let i = 0; i < size; i++) {
        let childNodes = currParent.ensureChildNodes();
        let childClone = childNodes.get(i).doClone(currParent);
        childNodes.set(i, childClone);
        nodesToProcess.add(childClone);
      }
    }

    return thisClone;
  }

  /**
   * Create a stand-alone, shallow copy of this node. None of its children (if any) will be cloned, and it will have
   * no parent or sibling nodes.
   * @return a single independent copy of this node
   * @see #clone()
   */
  public shallowClone(): Node {
    return this.doClone(null);
  }

  /*
     * Return a clone of the node using the given parent (which can be null).
     * Not a deep copy of children.
     */
  protected doClone(parent: Node): Node {
    let clone: Node

    try {
      clone = Object.assign({}, this);
    } catch (e) {

    }

    clone.parentNode = parent; // can be null, to create an orphan split
    clone.siblingIndex = parent == null ? 0 : this.siblingIndex;

    return clone;
  }

  /**
   * Start a depth-first traverse of the root and all of its descendants.
   * @param visitor Node visitor.
   * @param root the root node point to traverse.
   */
  public traverse(visitor: NodeVisitor, root: Node) {
    Validate.notNull(visitor)
    Validate.notNull(root)
    let node = root
    let depth = 0
    while (node !== undefined && node !== null) {
      // remember parent to find nodes that get replaced in .head
      let parent = node.parentNode
      let origSize = parent !== undefined && parent !== null ? parent.childNodeSize() : 0
      let next = node.nextSibling()
      // visit current node
      visitor.head(node, depth)
      if (parent !== undefined && parent !== null && !node.hasParent()) {
        // removed or replaced
        if (origSize === parent.childNodeSize()) {
          // replace ditches parent but keeps sibling index
          node = parent.childNode(node.getSiblingIndex())
        } else {
          // removed
          node = next
          if (node === undefined || node === null) {
            // last one, go up
            node = parent
            depth--
          }
          // don't tail removed
          continue
        }
      }
      if (node.childNodeSize() > 0) {
        node = node.childNode(0)
        depth++
      } else {
        while (true) {
          // as depth > 0, will have parent
          if (node === undefined || node === null) {
            throw new Error('node is null')
          }
          let nodeNext = node.nextSibling()
          if (!((nodeNext === undefined || nodeNext === null) && depth > 0)) {
            break
          }
          // when no more siblings, ascend
          visitor.tail(node, depth)
          node = node.getParentNode()
          depth--
        }
        visitor.tail(node, depth)
        if (node === root) {
          break
        }
        node = node.nextSibling()
      }
    }
  }

  /**
   * Start a depth-first filtering of the root and all of its descendants.
   * @param filter Node visitor.
   * @param root the root node point to traverse.
   * @return The filter result of the root node, or {@link FilterResult#STOP}.
   */
  public filterNode(filter: NodeFilter, root: Node): FilterResult {
    let node = root
    let depth = 0
    hilog.info(0x0000, 'testTag', '%{public}s', `Node filterNode start`);
    while (node !== undefined && node !== null) {
      hilog.info(0x0000, 'testTag', '%{public}s', `Node filterNode while start:${node.nodeName()}`);
      let result = filter.head(node, depth)
      if (result === FilterResult.STOP) {
        return result
      }
      // Descend into child nodes:
      if (result === FilterResult.CONTINUE && node.childNodeSize() > 0) {
        node = node.childNode(0)
        hilog.info(0x0000, 'testTag', '%{public}s', `Node filterNode while start1:${node.nodeName()}`);
        ++depth
        continue
      }
      // No siblings, move upwards:
      while (true) {
        // as depth > 0, will have parent
        if (node === undefined || node === null) {
          throw new Error('node is null')
        }
        if (!((node.nextSibling() === undefined || node.nextSibling() === null) && depth > 0)) {
          break
        }
        // 'tail' current node:
        if (result === FilterResult.CONTINUE || result === FilterResult.SKIP_CHILDREN) {
          result = filter.tail(node, depth)
          if (result === FilterResult.STOP) {
            return result
          }
        }
        // In case we need to remove it below.
        let prev = node
        node = node.getParentNode()
        depth--
        if (result === FilterResult.REMOVE) {
          // Remove AFTER finding parent.
          prev.remove()
        }
        // Parent was not pruned.
        result = FilterResult.CONTINUE
      }
      // 'tail' current node, then proceed with siblings:
      if (result === FilterResult.CONTINUE || result === FilterResult.SKIP_CHILDREN) {
        result = filter.tail(node, depth)
        if (result === FilterResult.STOP) {
          return result
        }
      }
      if (node === root) {
        return result
      }
      // In case we need to remove it below.
      let prev = node
      node = node.nextSibling()
      if (result === FilterResult.REMOVE) {
        // Remove AFTER finding sibling.
        prev.remove()
      }

    }
    hilog.info(0x0000, 'testTag', '%{public}s', `Node filterNode while end`);
    return FilterResult.CONTINUE
  }

}

class OuterHtmlVisitor implements NodeVisitor {
  private accum: string[];
  private out: OutputSettings;

  constructor(accum: string[], out: OutputSettings) {
    this.accum = accum
    this.out = out
    // out.prepareEncoder()
  }

  tail(node: Node, depth: number) {
    if (node.nodeName() !== '#text') {
      try {
        node.outerHtmlTail(this.accum, depth, this.out)
      } catch (e) {
        throw new Error("SerializationException")
      }
    }
  }

  head(node: Node, depth: number) {
    try {
      node.outerHtmlHead(this.accum, depth, this.out)
    } catch (e) {
      throw new Error(`Node head :${JSON.stringify(e)}`)
    }
  }
}

